Understanding Delegate Covariance

As you may have noticed, each of the delegates created thus far point to methods returning simple numerical data types (or void). However, assume you have a new Console Application named DelegateCovariance that defines a delegate type that can point to methods returning a custom class type (be sure to include your Car class definition in this new project):

// Define a delegate type pointing to methods that return Car objects.
public delegate Car ObtainCarDelegate();

Of course, you would be able to define a target for the delegate as expected:

namespace DelegateCovariance
{
    class Program
    {
        // Define a delegate type pointing to methods that return Car object.
        public delegate Car ObtainCarDelegate();

        static void Main(string[] args)
        {
            Console.WriteLine("***** Delegate Covariance *****\n");

            ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar);
            Car c = targetA();
            Console.WriteLine("Obtained a {0}", c);
            Console.ReadLine();
        }

        public static Car GetBasicCar()
        {
            return new Car("Zippy", 100, 55);
        }
    }
}

Now, what if you were to derive a new class from the Car type named SportsCar and you wanted to create a delegate type that can point to methods returning this class type? Prior to .NET 2.0, you would be required to define an entirely new delegate to do so, given that delegates were so type-safe that they did not honor the basic laws of inheritance:

// Define a new delegate type pointing to
// methods that return a SportsCar object.
public delegate SportsCar ObtainSportsCarDelegate();

As we now have two delegate types, we must create an instance of each to obtain Car and SportsCar types:

class Program
{
    public delegate Car ObtainCarDelegate();
    public delegate SportsCar ObtainSportsCarDelegate();

    public static Car GetBasicCar()
    { return new Car(); }

    public static SportsCar GetSportsCar()
    { return new SportsCar(); }

    static void Main(string[] args)
    {
        Console.WriteLine("***** Delegate Covariance *****\n");
        
        ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar);
        Car c = targetA();
        Console.WriteLine("Obtained a {0}", c);

        ObtainSportsCarDelegate targetB =
            new ObtainSportsCarDelegate(GetSportsCar);
        SportsCar sc = targetB();
        Console.WriteLine("Obtained a {0}", sc);
        Console.ReadLine();
    }
}

Given the laws of classic inheritance, it would be ideal to build a single delegate type that can point to methods returning either Car or SportsCar objects (after all, a SportsCar “is-a” Car). Covariance (which also goes by the term relaxed delegates) allows for this very possibility. Simply put, covariance allows you to build a single delegate that can point to methods returning class types related by classical inheritance.

Note In a similar vein, contravariance allows you to create a single delegate that can point to numerous methods that receive objects related by classical inheritance. Consult the .NET Framework 4.0 SDK documentation for further details.

class Program
{
    // Define a single delegate type that can point to
    // methods that return a Car or SportsCar.
    public delegate Car ObtainVehicleDelegate();
    
    public static Car GetBasicCar()
    { return new Car(); }

    public static SportsCar GetSportsCar()
    { return new SportsCar(); }

    static void Main(string[] args)
    {
        Console.WriteLine("***** Delegate Covariance *****\n");
        ObtainVehicleDelegate targetA = new ObtainVehicleDelegate(GetBasicCar);
        Car c = targetA();
        Console.WriteLine("Obtained a {0}", c);

        // Covariance allows this target assignment.
        ObtainVehicleDelegate targetB = new ObtainVehicleDelegate(GetSportsCar);
        SportsCar sc = (SportsCar)targetB();
        Console.WriteLine("Obtained a {0}", sc);
        Console.ReadLine();
    }
}

Notice that the ObtainVehicleDelegate delegate type has been defined to point to methods returning a strongly typed Car type. Given covariance, however, we can point to methods returning derived types as well. To obtain access to the members of the derived type, simply perform an explicit cast.

Source Code The DelegateCovariance project is located under the Chapter 11 subdirectory.